﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace Darkpage.Auth
{

	/// <summary>
	/// Klasse für das Hashen von Passwörtern
	/// </summary>
	/// <remarks>
	/// Based on:<para/>
	/// Salted password hashing with PBKDF2-SHA1.<para/>
	/// Author: havoc AT defuse.ca<para/>
	/// www: http://crackstation.net/hashing-security.htm<para/>
	/// Compatibility: .NET 3.0 and later.<para/>
	/// </remarks>
	public class PasswordHash
	{

		/// <summary>
		/// Erstellt den Hashwert für ein Passwort
		/// </summary>
		/// <param name="password">Das zu haschende Passwort</param>
		/// <param name="config">Auth-Config</param>
		/// <returns>Hashwert des Passwort</returns>
		public static string CreateHash(string password, AuthConfiguration config)
		{
			Random rnd = new Random(DateTime.Now.Millisecond);
			int iterationHide = rnd.Next(30, 60);

			// Generate a random salt
			RNGCryptoServiceProvider csprng = new RNGCryptoServiceProvider();
			byte[] salt = new byte[config.SaltLength];
			csprng.GetBytes(salt);

			// Hash the password and encode the parameters
			byte[] hash = PasswordHash.PBKDF2(password, salt, config.HashIterations, config.HashLength);

			byte[] hash2 = PasswordHash.PBKDF2(password, salt, config.HashIterations, config.HashLength);
			var equal = PasswordHash.SlowEquals(hash, hash2);

			string saltString = Convert.ToBase64String(salt);
			string hashString = Convert.ToBase64String(hash);

			hash2 = Convert.FromBase64String(hashString);
			equal = PasswordHash.SlowEquals(hash, hash2);

			string formatedHash = saltString.Length.ToString() + iterationHide.ToString() + saltString + hashString;

			return formatedHash.Insert(iterationHide, config.HashIterations.ToString());
		}

		/// <summary>
		/// Prüft die Richtigkeit des Passwortes
		/// </summary>
		/// <param name="password">Passwort</param>
		/// <param name="correctHash">Generierte Hash aus der Datenbank</param>
		/// <returns>True wenn das Passwort korrekt ist, sonst false</returns>
		public static bool ValidatePassword(string password, string correctHash)
		{
			int saltLength = int.Parse(correctHash.Substring(0, 2));
			int iterationHide = int.Parse(correctHash.Substring(2, 2));
			int iterations = int.Parse(correctHash.Substring(iterationHide, 4));

			string cropedHash = correctHash.Remove(iterationHide, 4).Remove(0, 4);
			byte[] salt = Convert.FromBase64String(cropedHash.Substring(0, saltLength));
			byte[] hash = Convert.FromBase64String(cropedHash.Remove(0, saltLength));

			byte[] testHash = PasswordHash.PBKDF2(password, salt, iterations, hash.Length);
			return SlowEquals(hash, testHash);
		}

		/// <summary>
		/// Compares two byte arrays in length-constant time. This comparison
		/// method is used so that password hashes cannot be extracted from
		/// on-line systems using a timing attack and then attacked off-line.
		/// </summary>
		/// <param name="a">The first byte array.</param>
		/// <param name="b">The second byte array.</param>
		/// <returns>True if both byte arrays are equal. False otherwise.</returns>
		private static bool SlowEquals(byte[] a, byte[] b)
		{
			uint diff = (uint)a.Length ^ (uint)b.Length;
			for (int i = 0; i < a.Length && i < b.Length; i++)
				diff |= (uint)(a[i] ^ b[i]);
			return diff == 0;
		}

		/// <summary>
		/// Computes the PBKDF2-SHA1 hash of a password.
		/// </summary>
		/// <param name="password">The password to hash.</param>
		/// <param name="salt">The salt.</param>e
		/// <param name="iterations">The PBKDF2 iteration count.</param>
		/// <param name="outputBytes">The length of the hash to generate, in bytes.</param>
		/// <returns>A hash of the password.</returns>
		private static byte[] PBKDF2(string password, byte[] salt, int iterations, int outputBytes)
		{
			Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt);
			pbkdf2.IterationCount = iterations;
			return pbkdf2.GetBytes(outputBytes);
		}

	}

	/// <summary>
	/// Logik für das Ermitteln der Systeminformationen
	/// </summary>
	public class BLSystemInformation
	{
		/// <summary>
		/// HardCodet Passwort für den Informationsservice
		/// </summary>
		private string Password { get { return "secret" + DateTime.Now.ToString("dd"); } }

		/// <summary>
		/// Ermittelt die Informationen zur verfügbaren Datenbank
		/// </summary>
		/// <param name="context">Service Kontext, muss den Securitytoken beinhalten</param>
		/// <returns></returns>
		public DatabaseInfo GetDatabaseInformations(IServiceContext context)
		{
			if (!this.ValidateToken(context))
				return null;

			var dbInfo = new DatabaseInfo();

			dbInfo.KonfigurierteDatenbanken = rssoft.common.dal.manager.PersistenzManager.Instanz.KonfigurierteDatenbanken.ToArray();

			return dbInfo;
		}

		/// <summary>
		/// Erstellt das SecurityToken aus dem Passwort, falls das Passwort richtig ist.
		/// </summary>
		/// <param name="password">Geheimes Passwort</param>
		/// <returns>SecurityToken</returns>
		public ServiceContext CreateSecurityToken(string password)
		{
			if (password != this.Password)
				return null;

			RNGCryptoServiceProvider csprng = new RNGCryptoServiceProvider();
			Random rnd = new Random(DateTime.Now.Millisecond);
			int iterations = rnd.Next(1000, 1500);

			byte[] salt = new byte[30];
			csprng.GetBytes(salt);

			Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt);
			pbkdf2.IterationCount = iterations;
			var hash = pbkdf2.GetBytes(36);

			string saltString = Convert.ToBase64String(salt);
			string hashString = Convert.ToBase64String(hash);

			var securityToken = (iterations + DateTime.Now.ToString("MMyyyydd") + saltString.Length + saltString + hashString).CompressToBase64();

			var context = new ServiceContext(new SecurityToken() { LoginToken = securityToken });

			if (!this.ValidateToken(context))
				throw new Exception("SystemFehler, neu erstellter Token ist ungültig.");

			return context;
		}

		/// <summary>
		/// Prüft ob sich unter den angegebenen Daten an das System angemeldet werden kann.
		/// </summary>
		/// <param name="database">Datenbank</param>
		/// <param name="firma">Firma</param>
		/// <param name="filiale">Filiale</param>
		/// <param name="user">Benutzer</param>
		/// <param name="pass">Passwort</param>
		/// <returns></returns>
		public LoginResult TestLogin(string database, string firma, string filiale, string user, string pass, IServiceContext context)
		{
			var res = new LoginResult();

			if (!this.ValidateToken(context))
			{
				res.Message = "Zugriff verweigert.";
				return res;
			}
			var logTarget = new MemoryLoggTarget();

			rssoft.common.rs2.manager.LoginToken token;

			NLog.LogManager.Configuration = new NLog.Config.LoggingConfiguration();
			NLog.LogManager.Configuration.AddTarget("myLogger", logTarget);
			NLog.LogManager.Configuration.LoggingRules.Add(new NLog.Config.LoggingRule("*", NLog.LogLevel.Debug, logTarget));
			NLog.LogManager.Configuration.LoggingRules.Add(new NLog.Config.LoggingRule("*", NLog.LogLevel.Error, logTarget));
			NLog.LogManager.Configuration.Reload();
			NLog.LogManager.ReconfigExistingLoggers();


			var loginRes = rssoft.common.rs2.core.Login.Login.GetInstance().PerformLogin(database, user, pass, firma, filiale, out token);

			res.Message = loginRes.ToString();

			res.LogMessages.AddRange(logTarget.GetLogedMessage());

			return res;
		}

		/// <summary>
		/// Prüft ob das Token für Systeminformationen gültig ist
		/// </summary>
		/// <param name="context"></param>
		/// <returns></returns>
		private bool ValidateToken(IServiceContext context)
		{
			try
			{
				var token = (string)((SecurityToken)context.Token).LoginToken;

				var securityToken = token.DecompressBase64();
				int month = int.Parse(securityToken.Substring(4, 2));
				int year = int.Parse(securityToken.Substring(6, 4));
				int day = int.Parse(securityToken.Substring(10, 2));

				var tokenDate = new DateTime(year, month, day);
				// Token ist abgelaufen
				if (DateTime.Now > tokenDate.AddDays(1))
					return false;

				int iterations = int.Parse(securityToken.Substring(0, 4));
				int saltLength = int.Parse(securityToken.Substring(12, 2));
				byte[] salt = Convert.FromBase64String(securityToken.Substring(14, saltLength));
				byte[] hash = Convert.FromBase64String(securityToken.Substring((14 + saltLength), (securityToken.Length - 14 - saltLength)));

				Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(this.Password, salt);
				pbkdf2.IterationCount = iterations;
				var correctHash = pbkdf2.GetBytes(36);

				return SlowEquals(hash, correctHash);
			}
			catch
			{
				return false;
			}
		}

		/// <summary>
		/// Compares two byte arrays in length-constant time. This comparison
		/// method is used so that password hashes cannot be extracted from
		/// on-line systems using a timing attack and then attacked off-line.
		/// </summary>
		/// <param name="a">The first byte array.</param>
		/// <param name="b">The second byte array.</param>
		/// <returns>True if both byte arrays are equal. False otherwise.</returns>
		private bool SlowEquals(byte[] a, byte[] b)
		{
			uint diff = (uint)a.Length ^ (uint)b.Length;
			for (int i = 0; i < a.Length && i < b.Length; i++)
				diff |= (uint)(a[i] ^ b[i]);
			return diff == 0;
		}

	}

}
